# -*- coding: utf-8 -*-
from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.parsers.rst import directives
import os
import re
from hashlib import sha1

import sphinx
from sphinx.builders import Builder


# note: Really a workaround for some internal Sphinx changes.
#       See VisualDirective.name_source_snippet for details
if sphinx.version_info < (1, 5, 0):
    from sphinx.ext.autodoc import AutodocReporter as ReporterInQuestion
else:
    from sphinx.util.docutils import LoggingReporter as ReporterInQuestion


try:
    from gen_example import render_snippet
except ImportError as error:
    render_snippet = None
    print (
        "Could not import snippet renderer. "
        "Will use static resources. Import error: %s" %
        error
    )


VISUAL_EXAMPLES_DIR = "visual_examples"

# todo: maybe should be more generic from sphinx conf
SOURCE_DIR = os.path.join(os.path.dirname(__file__))


def flag(argument):
    """Reimplement directives.flag to return True instead of None
    Check for a valid flag option (no argument) and return ``None``.
    (Directive option conversion function.)

    Raise ``ValueError`` if an argument is found.
    """
    if argument and argument.strip():
        raise ValueError('no argument is allowed; "%s" supplied' % argument)
    else:
        return True


def nonnegative_int_list(argument):
    if ',' in argument:
        entries = argument.split(',')
    else:
        entries = argument.split()
    return [directives.nonnegative_int(entry) for entry in entries]


def click_list(argument):
    value = nonnegative_int_list(argument)

    if len(value) != 2:
        ValueError("argument must contain 3 non-negative values")

    return value


class WrapsDirective(Directive):
    has_content = True

    def run(self):
        head = nodes.paragraph()
        head.append(nodes.inline("Wraps API:", "Wraps API: "))

        source = '\n'.join(self.content.data)
        literal_node = nodes.literal_block(source, source)
        literal_node['laguage'] = 'C++'

        return [head, literal_node]


class VisualDirective(Directive):
    has_content = True

    final_argument_whitespace = True
    option_spec = {
        'title': directives.unchanged,
        'introduction': directives.unchanged,
        'inter': directives.unchanged,

        'width': directives.positive_int,
        'height': directives.positive_int,
        'auto_layout': flag,
        'click': click_list,

    }

    def run(self):
        source = '\n'.join(self.content.data)
        literal = nodes.literal_block(source, source)
        literal['visualnodetype'] = True
        literal['language'] = 'python'

        # docutils document model is insane!
        head1 = nodes.paragraph()

        introduction = self.options.pop('introduction', "Example:")
        head1.append(nodes.inline(introduction, introduction))

        inter = self.options.pop('inter', "Outputs:")
        head2 = nodes.paragraph()
        head2.append(
            nodes.section("foo", nodes.inline(inter, inter))
        )

        directive_nodes = [
            head1,
            literal,
            head2,
            self.get_image_node(source)
        ]

        return directive_nodes

    def name_source_snippet(self, source):
        env = self.state.document.settings.env

        if (
            # note: This is series of hacks due to internal Sphinx changes
            #       In Sphinx==1.4.8 it was enough to check against
            #       AutodocReporter. Now it become really complicated.
            #       We should redo this in future if we will have more
            #       similar problems
            isinstance(self.state.reporter, ReporterInQuestion) and
            self.state.parent and self.state.parent.parent and
            self.state.parent.parent.children[0]['names']
        ):
            # If it is generated by autodoc then autogenerate title from
            # the function/method/class signature
            # note: hacky assumption that this is a signature node
            signature_node = self.state.parent.parent.children[0]
            signature = signature_node['names'][0]
            occurence = env.new_serialno(signature)

            name = signature + '_' + str(occurence)
        else:
            # If we could not quess then use explicit title or hexdigest
            name = self.options.get('title', sha1(source.encode()).hexdigest())

        return self.phrase_to_filename(name)

    def phrase_to_filename(self, phrase):
        """Convert phrase to normilized file name."""
        # remove non-word characters
        name = re.sub(r"[^\w\s\.]", '', phrase.strip().lower())
        # replace whitespace with underscores
        name = re.sub(r"\s+", '_', name)

        return name + '.png'

    def get_image_node(self, source):
        file_name = self.name_source_snippet(source)
        file_path = os.path.join(VISUAL_EXAMPLES_DIR, file_name)

        env = self.state.document.settings.env

        if all([
            render_snippet,
            env.config['render_examples'],
            not os.environ.get('SPHINX_DISABLE_RENDER', False),
        ]):
            try:
                render_snippet(
                    source, file_path,
                    output_dir=SOURCE_DIR, **self.options
                )
            except:
                print("problematic code:\n%s" % source)
                raise

        img = nodes.image()
        img['uri'] = "/" + file_path
        return img


class VisualBuilder(Builder):
    """
    Collects visual examples in the documentation for testing purpose.
    """
    name = 'vistest'

    def get_outdated_docs(self):
        return self.env.found_docs

    def write(self, build_docnames, updated_docnames, method='update'):
        # todo: monkey patching, rewrite
        self.snippets = []

        if build_docnames is None:
            build_docnames = sorted(self.env.all_docs)

        for docname in build_docnames:
            # no need to resolve the doctree
            doctree = self.env.get_doctree(docname)
            self.snippets.extend(self.collect_doc(docname, doctree))

    @staticmethod
    def traverse_condition(node):
        return isinstance(
            node, nodes.literal_block
        ) and 'visualnodetype' in node

    def collect_doc(self, docname, doctree):
        # Note(Sam): From Sphinx 6 we have to use doctree.findall() instead of doctree.traverse()
        # however sphinx 6 requires at least pyhton 3.8. This check will be removable once
        # we fully drop python 3.7 support
        if(callable(getattr(doctree,'findall',None))):
            return [
                (node.source, node.astext())
                for node in doctree.findall(self.traverse_condition)
            ]
        else:
            return [
                (node.source, node.astext())
                for node in doctree.traverse(self.traverse_condition)
            ]

def setup(app):
    app.add_config_value('render_examples', False, 'html')
    app.add_directive('wraps', WrapsDirective)
    app.add_directive('visual-example', VisualDirective)
    app.add_builder(VisualBuilder)

    return {'version': '0.1'}
